# 跨域
# 同源策略
编程中的同源,比较的是两个 url 是否同源。
主要看下面三个方面:
- 协议是否相同(http https file)
- 主机地址是否相同(www.xxx.com 127.0.0.1)
- 端口(0~65535)(http 默认端口是 80;https 默认端口是 443;MySQL 默认端口 3306)
如果两个 url 的协议、主机地址、端口都相同,那么这两个 url 是同源的,否则就是非同源。

违反了同源策略的请求,叫做跨域请求。
如果非同源,那么以下三种行为会受到限制:
- Cookie 无法操作
- DOM 无法操作
- Ajax 请求无效(请求可以发送,服务器也会处理这次请求,但是响应结果会被浏览器拦截)
# 解决跨域
主流的方案有两种:分别是 JSONP 和 CORS.
# JSONP
是程序员被迫想出来的解决跨域的方案。
JSONP 方案和 Ajax 没有任何关系
JSONP 方案只支持 GET 请求
JSONP 没有浏览器兼容问题,任何浏览器都支持。
原理
- 客户端利用 script 标签的 src 属性,去请求一个接口,因为 src 属性不受跨域影响。
- 服务端响应一个字符串
- 客户端接收到字符串,然后把它当做 JS 代码运行。
后端接口代码:
app.get("/api/jsonp", (req, res) => { // res.send('hello'); // res.send('console.log(1234)'); // res.send('abc()') // res.send('abc(12345)') // 接收客户端的函数名 let fn = req.query.callback; let obj = { status: 0, message: "登录成功" }; let str = JSON.stringify(obj); res.send(fn + `(${str})`); });前端代码:
<script> // 提前准备好一个函数 function xxx(res) { console.log(res); } </script> <script src="http://localhost:3006/api/jsonp?callback=xxx"></script>前端需要做什么?
- 如果使用 jQuery,$.ajax({ dataType: 'jsonp' }),必须指定 dataType 选项为 jsonp 即可
后端需要做什么?
- 如果使用 express,那么直接调用
res.jsonp(数据)即可。
- 如果使用 express,那么直接调用
# CORS
由于 XHR 对象被 W3C 标准化之后,提出了很多 XHR Level2 的新构想,其中新增了很多新方法(onload、response....)和 CORS 跨域资源共享。浏览器升级后开始支持 CORS 方案,从 IE10 开始支持。
CORS 方案,就是通过服务器设置响应头来实现跨域。
CORS 才是解决跨域的真正解决方案。
- 前端需要做什么?
- 无需做任何事情,正常发送 Ajax 请求即可。
- 后端需要做什么?
- 需要加响应头 (opens new window) 。或者使用第三方模块 cors 。
# 小结
| 方案 | 前端 | 后端 |
|---|---|---|
| CORS | × | 设置响应头 |
| JSONP(原生) | 1. 准备一个函数;2. 使用 script 的 src 发送请求 | 响应函数调用 |
| JSONP(jQuery) | 1. 还是调用$.ajax();2. 必须指定 dataType: 'jsonp' | res.jsonp(数据) |
# 防抖和节流
防抖和节流,作用类似,都是为了提高项目的性能。
# 防抖
当事件触发之后,约定单位时间(比如 1s)之后,执行里面的代码;如果在单位时间只内再次触发了事件,那么要重新计时,以保证事件里面的代码只执行一次。


<style>
* {
margin: 0;
padding: 0;
}
#box {
width: 500px;
margin: 20px auto;
}
ul,
li {
list-style: none;
}
input {
width: 100%;
height: 26px;
line-height: 26px;
}
li:hover {
background-color: beige;
}
ul {
display: none;
}
</style>
<div id="box">
<input type="text" id="ipt" />
<ul></ul>
</div>
<script src="./jquery.js"></script>
<script src="./template-web.js"></script>
<!-- 搜索建议模板 -->
<script type="text/html" id="tpl-list">
{{each result item}}
<li>{{item[0]}}</li>
{{/each}}
</script>
<script>
let timer = null;
// 当输入框的键盘弹起的时候,发送请求,获取搜索建议
$("#ipt").on("keyup", function() {
// 清楚前面的定时器
clearTimeout(timer);
// 获取输入的值(搜索关键字)
let keywords = $(this).val();
if (keywords === "") {
return $("ul")
.empty()
.hide();
}
// 如果关键字不为空,则获取搜索建议
// 约定 1s 之后发送请求
timer = setTimeout(() => {
$.ajax({
url: "https://suggest.taobao.com/sug",
data: { q: keywords, code: "utf-8" }, // 加入code参数,能够搜索多个汉字
dataType: "jsonp", // JSONP请求必须加这项
success: function(res) {
// console.log(res)
let str = template("tpl-list", res);
$("ul")
.html(str)
.show();
},
});
}, 1000);
});
</script>
# 节流
当事件触发之后,约定单位时间之内,事件里面的代码最多只能执行 1 次。
所以,节流减少了单位时间内代码的执行次数,从而提高性能。


使用 timer 当做开关(节流阀)。
- 开关 打开状态(timer = null),则允许执行代码。
- 开关是关闭状态(timer = 数字),则不允许执行代码。
代码:
<style>
html,
body {
height: 100%;
}
img {
position: absolute;
}
</style>
<img src="./angel.gif" alt="" />
<script src="./jquery.js"></script>
<script>
let timer = null; // null,表示节流阀打开状态,允许执行事件里面的代码
let img = $("img");
$(document).on("mousemove", function(e) {
//
console.log(111);
// 当事件触发了,判断一下,节流阀的状态,如果是关闭状态,则不允许创建另一个定时器
if (timer !== null) return;
timer = setTimeout(() => {
console.log(222);
let x = e.pageX;
let y = e.pageY;
// 设置图片的css(left和top)
img.css({ left: x + "px", top: y + "px" });
// 当定时器执行完毕,重新打开节流阀
timer = null;
}, 16);
});
</script>
# ES6 降级处理
因为 ES 6 有浏览器兼容性问题,可以使用一些工具进行降级处理,例如:babel
降级处理 babel 的使用步骤
- 安装 Node.js
- 命令行中安装 babel
- 配置文件
.babelrc - 运行
项目初始化 (项目文件夹不能有中文)
npm init -y在命令行中,安装 babel babel 官网 (opens new window)
npm install @babel/core @babel/cli @babel/preset-env配置文件
.babelrc(手工创建这个文件)babel 的降级处理配置
{ "presets": ["@babel/preset-env"] }在命令行中,运行
# 把转换的结果输出到指定的文件 npx babel index.js -o test.js # 把转换的结果输出到指定的目录 npx babel 包含有js的原目录 -d 转换后的新目录
# Promise
Promise 能够处理异步程序。
# 回调地狱

JS 中或 node 中,都大量的使用了回调函数进行异步操作,而异步操作什么时候返回结果是不可控的,如果我们希望几个异步请求按照顺序来执行,那么就需要将这些异步操作嵌套起来,嵌套的层数特别多,就会形成回调地狱 或者叫做 横向金字塔。
下面的案例就有回调地狱的意思:
案例:有 a.txt、b.txt、c.txt 三个文件,使用 fs 模板按照顺序来读取里面的内容,代码:
// 将读取的a、b、c里面的内容,按照顺序输出
const fs = require("fs");
// 读取a文件
fs.readFile("./a.txt", "utf-8", (err, data) => {
if (err) throw err;
console.log(data.length);
// 读取b文件
fs.readFile("./b.txt", "utf-8", (err, data) => {
if (err) throw err;
console.log(data);
// 读取c文件
fs.readFile("./c.txt", "utf-8", (err, data) => {
if (err) throw err;
console.log(data);
});
});
});
案例中,只有三个文件,试想如果需要按照顺序读取的文件非常多,那么嵌套的代码将会多的可怕,这就是回调地狱的意思。
# Promise 简介
- Promise 对象可以解决回调地狱的问题
- Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大
Promise可以理解为一个容器,里面可以编写异步程序的代码- 从语法上说,Promise 是一个对象,使用的使用需要
new
# Promise 简单使用
Promise 是“承诺”的意思,实例中,它里面的异步操作就相当于一个承诺,而承诺就会有两种结果,要么完成了承诺的内容,要么失败。
所以,使用 Promise,分为两大部分,首先是有一个承诺(异步操作),然后再兑现结果。
第一部分:定义“承诺”
// 实例化一个Promise,表示定义一个容器,需要给它传递一个函数作为参数,而该函数又有两个形参,通常用resolve和reject来表示。该函数里面可以写异步请求的代码
// 换个角度,也可以理解为定下了一个承诺
let p = new Promise((resolve, reject) => {
// 形参resolve,单词意思是 完成
// 形参reject ,单词意思是 失败
fs.readFile("./a.txt", "utf-8", (err, data) => {
if (err) {
// 失败,就告诉别人,承诺失败了
reject(err);
} else {
// 成功,就告诉别人,承诺实现了
resolve(data.length);
}
});
});
第二部分:获取“承诺”的结果
// 通过调用 p 的then方法,可以获取到上述 “承诺” 的结果
// then方法有两个函数类型的参数,参数1表示承诺成功时调用的函数,参数2可选,表示承诺失败时执行的函数
p.then(
(data) => {},
(err) => {}
);
完整的代码:
const fs = require("fs");
// promise 承诺
// 使用Promise分为两大部分
// 1. 定义一个承诺
let p = new Promise((resolve, reject) => {
// resolve -- 解决,完成了; 是一个函数
// reject -- 拒绝,失败了; 是一个函数
// 异步操作的代码,它就是一个承诺
fs.readFile("./a.txt", "utf-8", (err, data) => {
if (err) {
reject(err);
} else {
resolve(data.length);
}
});
});
// 2. 兑现承诺
// p.then(
// (data) => {}, // 函数类似的参数,用于获取承诺成功后的数据
// (err) => {} // 函数类型的参数,用于或承诺失败后的错误信息
// );
p.then(
(data) => {
console.log(data);
},
(err) => {
console.log(err);
}
);
# then 方法的链式调用
前一个 then 里面返回的字符串,会被下一个 then 方法接收到。但是没有意义;
前一个 then 里面返回的 Promise 对象,并且调用 resolve 的时候传递了数据,数据会被下一个 then 接收到
前一个 then 里面如果没有调用 resolve,则后续的 then 不会接收到任何值
const fs = require("fs"); // promise 承诺 new Promise((resolve, reject) => { fs.readFile("./a.txt", "utf-8", (err, data) => { err ? reject(err) : resolve(data.length); }); }) .then((a) => { console.log(a); return new Promise((resolve, reject) => { fs.readFile("./a.txt", "utf-8", (err, data) => { err ? reject(err) : resolve(data.length); }); }); }) .then((b) => { console.log(b); return new Promise((resolve, reject) => { fs.readFile("./a.txt", "utf-8", (err, data) => { err ? reject(err) : resolve(data.length); }); }); }) .then((c) => { console.log(c); }) .catch((err) => { console.log(err); });catch 方法可以统一获取错误信息
# 封装按顺序异步读取文件的函数
function myReadFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, "utf-8", (err, data) => {
err ? reject(err) : resolve(data.length);
});
});
}
myReadFile("./a.txt")
.then((a) => {
console.log(a);
return myReadFile("./b.txt");
})
.then((b) => {
console.log(b);
return myReadFile("./c.txt");
})
.then((c) => {
console.log(c);
})
.catch((err) => {
console.log(err);
});
# async 和 await 修饰符
ES6 --- ES2015
async 和 await 是 ES2017 中提出来的。
异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题。
从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。
异步 I/O 不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是不是异步。
==async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案==。
ES2017 提供了 async 和 await 关键字。await 和 async 关键词能够将异步请求的结果以返回值的方式返回给我们。
- async 用于修饰一个 function
- async 修饰的函数,一般表示该函数里面有异步操作(Promise 的调用)
await和async需要配合使用,没有async修饰的函数中使用await是没有意义的,会报错- await 需要定义在 async 函数内部,await 后面跟的一般都是一个函数(函数里面包含有 Promise)的调用
- await 修饰的异步操作,可以使用返回值的方式去接收异步操作的结果
- 如果有哪一个 await 操作出错了,会中断 async 函数的执行
总结来说:async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
const fs = require("fs");
// 将异步读取文件的代码封装
function myReadFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, "utf-8", (err, data) => {
err ? reject(err) : resolve(data.length);
});
}).catch((err) => {
console.log(err);
});
}
async function abc() {
let a = await myReadFile("./a.txt");
let b = await myReadFile("./b.txt");
let c = await myReadFile("./c.txt");
console.log(b);
console.log(a);
console.log(c);
}
abc();
# Promise 小结
- 封装函数
- 函数中返回 Promise 对象
- Promise 对象里面写你的
异步代码
- Promise 对象里面写你的
- 函数中返回 Promise 对象
- 使用 async 修改一个函数
- 函数里面使用 await 修饰 Promise 对象(调用前面封装的函数)
- 以返回值的方式获取异步代码成功的结果了
# 身份认证(了解)
# 开发模式
- 传统的服务端渲染模式
- 后端的接口和前端的代码在一起(服务器)
- 涉及不到跨域
- 有利于 SEO
- 客户端(前端)不需要渲染数据,如果是手机,将会非常省电。运行速度非常快。
- 缺点是开发效率低。
- 适合使用 cookie 或 session 身份认证
- 新型的前后端分离模式
- 前端代码单独在一个文件夹(服务器)(自己的电脑上)
- 后端的接口在另外的文件夹(服务器)(刘龙宾老师的服务器上)
- 开发速度快,适合多人协作开发。
- 适合使用 JWT(json web token) 方式的身份认证。
PS:一个项目到底该使用哪种开发模式?
- 不能一概而论,比如有的网站,首页为了 SEO 采用传统的服务端渲染模式,其他页面采用前后端分离模式。
- 后台管理系统,涉及不到 SEO,可以采用前后端分离模式。
- 小型企业网站,可以采用传统的服务端渲染模式。
# 演示传统的服务端渲染模式
最大的特点
服务端代码和前端代码在同一个服务器(文件夹)
搭建服务器
| - app.js (搭建服务器) | - public (public文件夹用于存放前端页面) | - index.html (一个前端的html页面)app.js 编写接口
/index.html- 接口中,使用 fs 读取文件,并替换内容,最后响应给客户端
- 客户端请求
http://localhost:3006/index.html
// 接口,提供index.html 页面
app.get("/index.html", (req, res) => {
// 客户端发来请求,希望看到index.html 页面。
// 服务器,把html页面读取出来,把读取的结果响应给客户端即可
fs.readFile("./public/index.html", "utf-8", (err, data) => {
if (err) throw err;
// console.log(data);
// 假设从数据库中查询到了标题和内容
data = data.replace("{{title}}", "咏鹅");
data = data.replace("{{content}}", "鹅鹅鹅,曲项向天歌");
res.send(data);
});
});
页面中的数据,是在服务端完成渲染的,客户端接收到的已经是一个包含数据的完整页面了,所以叫做服务端渲染模式。
# Cookie
# 原理图
身份认证,要完成的是:不登录,不允许访问其他页面。

# 实现身份认证
- 搭建基础的服务器(或者直接使用前面的 传统服务端渲染模式 代码)
- 中间件配置 cookie-parser
app.use(cookieParser())
- 模拟一个登录接口
- 如果登录成功,设置 cookie。
res.cookie('key', 'value', 配置项);
- 如果登录成功,设置 cookie。
- /index.html 接口中,根据 cookie 判断是否登录,从而完成身份认证
详见代码
# 优缺点
- 优点
- 体积小
- 客户端存放,不占用服务器空间
- 浏览器会
自动携带,不需要写额外的代码,比较方便
- 缺点
- 客户端保存,安全性较低。但可以存放加密的字符串来解决
- 只能存字符串,cookie 的大小也是有限制的
- 可以实现跨域,但是难度大,难理解,代码难度高
- 不适合前后端分离式的开发
# 适用场景
- 传统的服务器渲染模式
- 存储安全性较低的数据,比如视频播放位置等
# Session 身份认证
# 原理图
要实现的效果:不登录,不允许访问其他接口

# 实现身份认证
搭建基础的服务器
- 下载安装第三方模块
express和express-session - 创建 app.js
- 加载所需模块
const express = require('express');const session = require('express-session');
- 下载安装第三方模块
中间件配置 session
app.use( session({ secret: "adfasdf", // 这个随便写 saveUninitialized: false, resave: false, }) );完成登录接口
如果登录成功,使用 session 记录用户信息。
req.session.isLogin = true; req.session.username = "laotang";
/index.html 接口中,根据 session 判断是否登录,从而完成身份认证
详见代码
# 优缺点
- 优点
- 服务端存放,安全性较高
- 浏览器会自动携带 cookie,不需要写额外的代码,比较方便
- 适合服务器端渲染模式
- 缺点
- 会占用服务器端空间
- session 实现离不开 cookie,如果浏览器禁用 cookie,session 不好实现
- 不适合前后端分离式的开发
# 适用场景
- 传统的服务器渲染模式
- 安全性要求较高的数据可以使用 session 存放,比如用户私密信息、验证码等